
package com.studiofortress.sf.structure;

import com.studiofortress.sf.graphics.GraphicsGL;
import com.studiofortress.sf.graphics.display.ControlEvent;
import com.studiofortress.sf.graphics.display.GraphicsDisplay;
import com.studiofortress.sf.util.collections.ObjectStack;
import java.util.ArrayList;
import java.util.List;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLEventListener;

/**
 * This is the top level component that can automatically hold and run a World
 * object.
 *
 * This can also hold MainLoopEvents which are called once before each frame.
 * These are meant as places to put game-system wide code which does not relate
 * to the actual gameplay itself. For example frame limiting or delta-time
 * calculating code. Any events that relate to altering the game itself (like
 * updating a Player Actor in the World) should be added into the World that is
 * run inside of this MainLoop.
 *
 * The G generic type allows the developer to create their own GraphicsGL object
 * with extra methods, and it will be seen as it's type across the entire game.
 * If a generic type other then GraphicsGL is used then the createGraphics
 * method must be overridden to return an instance of the new type used.
 *
 * @author Joseph Lenton
 * @param <G> The type of graphics object to use when drawing.
 */
public class MainLoop<G extends GraphicsGL>
{
    // if run is called but the display isn't displaying, this is how long we
    // should give it to see if it becomes running
    // in milliseconds
    private static final long IS_RUNNING_WAIT_TIME = 2000;
    // how long to sleep for between each check, in milliseconds
    private static final long IS_RUNNING_SLEEP_TIME = 100;
    
    private GraphicsDisplay display;
    
    private boolean isRunning;
    private World<G, ?> world;
    
    private WorldPainter painter;

    private List<MainLoopEvent> events;

    private ObjectStack<ControlEvent> eventStack;
    
    /**
     * Creates a new MainLoop with the given GraphicsDisplay. When a World
     * is set to this MainLoop it will draw the World to the
     * GraphicsDisplay.
     * @param display The GraphicsDisplay this MainLoop will draw too. Cannot be null.
     */
    public MainLoop(GraphicsDisplay display)
    {
        if (display == null) {
            throw new IllegalArgumentException("The given display cannot be null.");
        }
        
        this.display = display;
        painter = new WorldPainter();
        display.getCanvas().addGLEventListener(painter);
        
        isRunning = false;
        
        world = null;
        
        events = new ArrayList<MainLoopEvent>();
        eventStack = new ObjectStack<ControlEvent>();
    }

    /**
     * Adds the given event to this MainLoop to be called once before each frame.
     * @param event The event to add to this MainLoop, cannot be null.
     */
    public void addEvent(MainLoopEvent event)
    {
        if (event == null) {
            throw new IllegalArgumentException("The given event cannot be null.");
        }
        events.add(event);
    }

    /**
     * Finds and removes the given event from this MainLoop.
     * @param event The event to remove, cannot be null.
     */
    public void removeEvent(MainLoopEvent event)
    {
        if (event == null) {
            throw new IllegalArgumentException("The given event cannot be null.");
        }
        events.remove(event);
    }

    /**
     * @return A list containing all the MainLoopEvents in this MainLoop in the order they will be called.
     */
    public List<MainLoopEvent> getEvents()
    {
        List<MainLoopEvent> returnList = new ArrayList<MainLoopEvent>();
        returnList.addAll(events);
        return returnList;
    }

    /**
     * This returns the first event that matches the given type. If multiple
     * instances of the given class are stored in this MainLoop then subsequent
     * calls to this method do not need to guarantee that the same instance is
     * always returned.
     * @param <A> The type being searched for.
     * @param eventClass The class of the event to find and return, cannot be null.
     * @return Null if no instance was found, otherwise the first MainLoopEvent found that matches the given class.
     */
    public <A> A getEvent( Class<A> eventClass )
    {
        if (eventClass == null) {
            throw new IllegalArgumentException("The given class cannot be null");
        }

        for ( MainLoopEvent event : events ) {
            if ( eventClass.isInstance(event) ) {
                return (A) event;
            }
        }

        return null;
    }

    /**
     * A helper function to make running the MainLoop a little more trivial.
     * This is the equivalent of calling setWorld and then run together.
     * @param world The World to set to this MainLoop and then run. Cannot be null.
     */
    public void run(World<G, ?> world)
    {
        if (world == null) {
            throw new IllegalArgumentException("The given world cannot be null.");
        }
        setWorld( world );
        run();
    }

    /**
     * Runs the World stored in this MainLoop.
     */
    public void run()
    {
        if (!hasWorld()) {
            throw new IllegalStateException("Should not run the MainLoop without a world.");
        }

        // if the display isn't on screen then wait a little first
        // the display might have _just_ been added to a frame and so is still
        // being added to the display device
        if (!display.isRunning()) {
            long waitTime = System.currentTimeMillis() + IS_RUNNING_WAIT_TIME;
            while (!display.isRunning() && waitTime < System.currentTimeMillis()) {
                try {
                    Thread.sleep(IS_RUNNING_SLEEP_TIME);
                } catch (InterruptedException e) {
                    // do nothing
                }
            }
        }

        isRunning = true;
        while (isRunning && hasWorld() && display.isRunning()) {
            runFrame();
        }
        isRunning = false;
    }

    /**
     * This contains all the code for running a single frame in the MainLoop.
     * This includes updating the events, acting the world and then painting it.
     *
     * If the current world returned by getWorld() is null then nothing will
     * happen.
     */
    protected void runFrame()
    {
        // makes a local copy because this MainLoops World might change during the act
        World currentWorld = getWorld();
        if (currentWorld != null) {
            // If we are still painting the previous frame, we wait till it's done.
            while ( painter.isPainting() ) {
                Thread.yield();
            }
            
            // run all mainloop events
            for ( MainLoopEvent event : events ) {
                event.action( this );
            }
            
            display.updateControls( eventStack );
            while ( eventStack.size() > 0 ) {
                currentWorld.handleEvent( eventStack.pop() );
            }
            currentWorld.act();

            // wait till the last buffer has flipped
            while ( painter.isWaitingOnFlip() ) {
                Thread.yield();
            }

            // this calls the WorldPainter to paint getWorld and then flip
            painter.paint(currentWorld);
            display.paint();
        }
    }
    
    /**
     * Sets the world to run in this MainLoop. The Worlds act and paint
     * methods will be automatically called by this MainLoop.
     * Passing in null sets that there is no world and if this MainLoop is
     * currently running then it will stop running.
     * If the given World is already within a different MainLoop then it
     * will be removed from that MainLoop before being added to this one.
     * @param world The World to run by this MainLoop or null if you don't want it to run a World.
     */
    public void setWorld(World<G, ?> world)
    {
        if (this.world == world) {
            return;
        }

        // remove the new world from the container it is in
        if (world != null) {
            if (world.isInMainLoop()) {
                world.getMainLoop().setWorld(null);
            }
        }

        // remove the current world from this MainLoop
        if (this.world != null) {
            if (this.world.isInMainLoop()) {
                this.world.setMainLoop(null);
                this.world.removedFromWorld();
            }
        }

        // set the new world
        this.world = world;
        if (this.world != null) {
            this.world.setMainLoop(this);
            this.world.addedToWorld();
        }
    }

    /**
     * @return Null if there is no world set, otherwise the World this MainLoop will run.
     */
    public World<G, ?> getWorld()
    {
        return world;
    }

    /**
     * @return True if there is a World set, false if not (the World is null).
     */
    public boolean hasWorld()
    {
        return getWorld() != null;
    }
    
    /**
     * @return The screen that this MainLoop draws too.
     */
    public GraphicsDisplay getDisplay()
    {
        return display;
    }
    
    /**
     * Stops this MainLoop from running, if it's currently running.
     */
    public void stopRunning()
    {
        isRunning = false;
    }
    
    /**
     * @return True if the this MainLoop is currently running, otherwise false.
     */
    public boolean isRunning()
    {
        return isRunning;
    }

    /**
     * This is the standard method where the GraphicsGL used for drawing is
     * created.
     * @param gl The GL object this will be wrapping and making it's calls too.
     * @return A new GraphicsGL object which can be used for drawing too.
     */
    protected G createGraphics(GL gl)
    {
        return (G) new GraphicsGL( gl );
    }
    
    /**
     * An EventListener added to the GLCanvas to draw the World that the
     * MainLoop is holding.
     *
     * This works by having the MainLoop set the world which the WorldPainter
     * will paint once. After it has been painted the MainLoop will have to
     * reset the World to this painter for it to paint a second time.
     *
     * This allows the MainLoop to have complete control over when the World
     * will and will not be painted.
     */
    private class WorldPainter implements GLEventListener
    {
        private boolean flipWait;

        private World world;

        private GraphicsGL graphics;

        /**
         * Sets a World to be painted. This is automatically done on the next
         * time that the display method is called.
         * This must be called on <b>each<b> time the World is to be painted.
         * If null is passed in then nothing will be painted.
         * @param world The World to be painted.
         */
        public void paint(World<G, ?> world)
        {
            this.world = world;
            flipWait = true;
        }

        /**
         * @return True if this is either currently painting or about to paint.
         */
        public boolean isPainting()
        {
            return world != null;
        }

        public boolean isWaitingOnFlip()
        {
            return flipWait;
        }

        @Override
        public void init(GLAutoDrawable drawable)
        {
            GL gl = drawable.getGL();
            gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
            gl.glShadeModel(GL.GL_FLAT);
            graphics = createGraphics( gl );
        }

        @Override
        public void display(GLAutoDrawable drawable)
        {
            if (isPainting()) {
                final GL gl = drawable.getGL();
                
                // Clear the drawing area
                gl.glClear( GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT );
                gl.glMatrixMode( GL.GL_MODELVIEW );
                gl.glLoadIdentity();
                
                // paint the current world
                world.realPaint( graphics );

                /* This must be done _before_ we call flush, cos from this point
                 * on it doesn't matter if we start updating logic. However
                 * flush will take a while if we are waiting on the vsync. */
                world = null;
                // done drawing
                graphics.flush();
                flipWait = false;
            }
        }

        @Override
        public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) { }

        @Override
        public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) { }
    }
}
